了解了如何使用USM内存后，来讨论下如何管理数据。可以将其分为两部分:数据初始化和数据移动。\par

\hspace*{\fill} \par %插入空行
\textbf{初始化}

数据初始化关注的是对内存执行计算前内存的填充值，常见初始化是使用零填充。要对使用USM内存进行初始化，可以通过多种方式来实现。最直接的就是写一个内核来做初始化，如果数据集特别大，或者需要复杂的计算，这种方法没问题。也可以实现为遍历所有元素的循环，将每个元素设置为0，这种方法存在一个问题。循环可以很好地用于主机和共享分配的内存，因为可以在主机上访问。但在主机上不能访问设备分配，主机代码中的循环将不能对设备内存进行写入。这就有了第三种选择。\par

memset函数可以用来实现这个初始化模式。USM提供了一个memset函数，有三个参数:要设置的内存指针，要设置的模式，以及要设置为该模式的字节数。与主机的循环不同，memset是并行发生的。\par

虽然memset是一个有用的操作，但只允许指定一个模式来填充。USM还提供了fill方法，允许用任意模式填充内存。给它创建一个int型模板，然后用数字“42”填充内存。与memset类似，fill接受三个参数:要填充的内存的指针，要填充的值，以及希望将该值写入内存的数量。\par

\hspace*{\fill} \par %插入空行
\textbf{数据移动}

数据移动可能是USM的重点。如果正确的数据没有在正确的时间出现在正确的地点，程序将产生不正确的结果。USM定义了两种用来管理数据的策略:显式和隐式。使用策略的选择与硬件支持，或使用的USM类型有关。\par

\hspace*{\fill} \par %插入空行
\textbf{显式}

USM提供的第一个策略是显式数据移动(图6-6)，必须显式地在主机和设备之间复制数据。可以通过调用memcpy完成，该方法可以在处理程序和队列类上找到。memcpy方法有三个参数:指向目标内存的指针，指向源内存的指针，以及主机和设备之间复制的字节数。不需要指定复制发生的方向——这在源指针和目标指针中是隐式确定的。\par

显式数据移动最常见的用法是使用USM对设备内存中的数据进行复制，因为设备端内存在主机上不可访问。此外，这可能会造成错误:可能会忽略复制，不正确的数据量可能被复制，或者源或目标指针可能不正确。\par

然而，显式数据移动也有优点:完全控制数据移动。某些应用程序中，控制复制数据的数量和复制数据的时间，对于获得最佳性能非常重要。理想情况下，可以将计算与数据移动重叠，确保硬件高效率运行。\par

其他的USM类型，不论是host，还是shared，都可以在主机和设备端访问，不需要显式地复制到设备。这就引出了USM中数据移动的另一种策略。\par

\hspace*{\fill} \par %插入空行
图6-6 USM显式移动数据
\begin{lstlisting}[caption={}]
constexpr int N = 42;

queue Q;

std::array<int,N> host_array;
int *device_array = malloc_device<int>(N, Q);
for (int i = 0; i < N; i++)
	host_array[i] = N;

Q.submit([&](handler& h) {
	// copy hostArray to deviceArray
	h.memcpy(device_array, &host_array[0], N * sizeof(int));
});

Q.wait(); // needed for now (we learn a better way later)

Q.submit([&](handler& h) {
	h.parallel_for(N, [=](id<1> i) {
		device_array[i]++;
	});
});

Q.wait(); // needed for now (we learn a better way later)

Q.submit([&](handler& h) {
	// copy deviceArray back to hostArray
	h.memcpy(&host_array[0], device_array, N * sizeof(int));
});

Q.wait(); // needed for now (we learn a better way later)

free(device_array, Q);
\end{lstlisting}

\hspace*{\fill} \par %插入空行
\textbf{隐式}

USM提供的第二种策略是隐式数据移动(示例用法如图6-7所示)。这个策略中，数据移动是隐式发生的，不需要memcpy，因为可以通过USM指针直接访问数据。而系统的任务是确保数据在使用时，在正确的位置上可用。\par

对于主机内存，可能会争论是否真的进行了数据移动。根据定义，分配的内存始终指向主机内存，因此给定的主机指针表示的内存不能存储在设备上。但当在设备上访问主机内存时，数据移动就会发生。不是将内存迁移到设备，而是通过适当的接口将读或写的值传输到内核中。这对于数据不需要驻留在设备上的流内核很有用。\par

隐式数据移动主要与USM共享内存有关。这种类型的内存可以在主机和设备上访问，并且可以在主机和设备之间迁移。这种迁移是自动进行的，或者是隐式地进行的，只需访问不同位置的数据即可。接下来，讨论为共享内存进行数据迁移时需要考虑的几个问题。\par

\hspace*{\fill} \par %插入空行
图6-7 USM隐式数据移动
\begin{lstlisting}[caption={}]
constexpr int N = 42;

queue Q;

int* host_array = malloc_host<int>(N, Q);
int* shared_array = malloc_shared<int>(N, Q);
for (int i = 0; i < N; i++)
	host_array[i] = i;
	
Q.submit([&](handler& h) {
	h.parallel_for(N, [=](id<1> i) {
		// access sharedArray and hostArray on device
		shared_array[i] = host_array[i] + 1;
	});
});

Q.wait();

free(shared_array, Q);
free(host_array, Q);
\end{lstlisting}

\hspace*{\fill} \par %插入空行
\textbf{迁移}

通过显式数据移动，可以控制发生多少数据移动。使用隐式数据移动，系统可处理这一问题，但可能没有那么高效。DPC++运行时不是oracle——不能预测应用将访问什么数据。此外，指针分析对于编译器来说非常困难，可能无法准确地分析和识别内核中可能使用的每个内存分配。因此，隐式数据移动机制的实现，可能会根据支持USM设备的功能做出不同的决策，这既影响共享内存的使用方式，也影响了它们的执行方式。\par

如果设备能力非常强，能够根据需要迁移内存。这种情况下，数据移动将在主机或设备试图访问数据不存在的内存位置时发生。按需获取数据极大地简化了编程，可以在任何地方访问USM共享内存，并且可以正常工作。如果设备不支持按需迁移(第12章解释了如何查询设备的功能)，仍然能够保证相同的语义，并对共享指针的使用方式进行限制。\par

限制形式的USM共享内存会确定何时何地可以访问共享neicu8n，以及共享分配的大小。如果设备不能按需迁移内存，则运行时必须保守，并假定内核可以访问其设备附加内存中的任何分配。这会带来两种后果。\par

首先，主机和设备不该同时访问共享内存，程序应该以阶段替代访问。主机可以访问内存数据，然后内核可以使用该数据进行计算，最后主机读取结果。\par

如果没有这个限制，主机可以访问内核的不同分配。这种并发访问通常发生在设备内存页上。主机可以访问一个内存页，而设备可以访问另一个内存页。第19章将介绍原子访问相同的数据块。\par

这种受限的共享内存形式的第二个后果是，受到设备内存总量的限制。如果设备不能按需迁移内存，则无法将数据迁移到主机，为不同的数据腾出空间。如果设备支持按需迁移，则可能会超量使用内存，从而允许内核计算超过设备内存通常包含的数据，而这种灵活性可能会因为数据移动，产生性能损失。\par

\hspace*{\fill} \par %插入空行
\textbf{细粒度控制}

当设备支持按需迁移共享内存时，访问内存位置上没有相应数据时，需要进行数据移动。这时，内核在等待数据移动完成时可能会停止。接下来执行的语句可能会产生更多的数据移动，并给内核执行带来更多的延迟。\par

DPC++提供了一种修改自动迁移机制性能的方法。通过定义两个函数来做到这一点:prefetch和mem\_advise。图6-8展示了每种方法，这些函数向运行时提供了内核如何访问数据的方式，以便运行时选择在内核访问数据之前开始移动数据。请注意，这个例子直接在队列对象上调用parallel\_for的队列快捷方法，而不是在传递给submit(命令组)一个Lambda调用。\par

\hspace*{\fill} \par %插入空行
图6-8 通过prefetch和mem\_advise进行细粒度控制
\begin{lstlisting}[caption={}]
// Appropriate values depend on your HW
constexpr int BLOCK_SIZE = 42;
constexpr int NUM_BLOCKS = 2500;
constexpr int N = NUM_BLOCKS * BLOCK_SIZE;

queue Q;
int *data = malloc_shared<int>(N, Q);
int *read_only_data = malloc_shared<int>(BLOCK_SIZE, Q);

// Never updated after initialization
for (int i = 0; i < BLOCK_SIZE; i++)
	read_only_data[i] = i;
	
// Mark this data as "read only" so the runtime can copy it
// to the device instead of migrating it from the host.
// Real values will be documented by your DPC++ backend.
int HW_SPECIFIC_ADVICE_RO = 0;

Q.mem_advise(read_only_data, BLOCK_SIZE, HW_SPECIFIC_ADVICE_RO);

event e = Q.prefetch(data, BLOCK_SIZE);

for (int b = 0; b < NUM_BLOCKS; b++) {
	Q.parallel_for(range{BLOCK_SIZE}, e, [=](id<1> i) {
		data[b * BLOCK_SIZE + i] += data[i];
	});
	if ((b + 1) < NUM_BLOCKS) {
		// Prefetch next block
		e = Q.prefetch(data + (b + 1) * BLOCK_SIZE, BLOCK_SIZE);
	}
}

Q.wait();

free(data, Q);
free(read_only_data, Q);
\end{lstlisting}

最简单的方法是调用预取(prefetch)。此函数作为处理程序或队列类的成员函数，接受指针和字节数。这可以通知运行时某些数据将在设备上使用，以便能够及时地迁移。理想情况下，应该尽早了解这些信息，这样当内核访问数据时，数据就已经驻留在设备上了，从而消除之前所说的延迟。\par

DPC++提供的另一个函数是mem\_advise，这个函数可以确定内核中可以使用哪些特定于设备的内存。这样的话，当数据只在内核中读取，而不是写入，系统就可以意识到这个操作可以复制设备上的数据，这样在内核完成后就不需要更新主机的数据。但是，传递给mem\_advise的参数是特定于设备的，所以使用此函数之前，请查阅硬件供应商的文档。\par
















